/*
 *   Copyright (c) 2011 Hitachi Data Systems, Inc.
 *
 *   Permission is hereby granted to  this software and associated
 *   documentation files (the "Software"), subject to the terms and
 *   conditions of the Sample Source Code License (SSCL) delivered
 *   with this Software. If you do not agree to the terms and
 *   conditions of the SSCL,
 *
 *     (i)  you must close this file and delete all copies of the
 *          Software, and
 *     (ii) any permission to use the Software is expressly denied.
 *
 */

package com.hds.hcp.apihelpers;

import java.io.IOException;
import java.io.InputStream;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.DeflaterInputStream;

/**
 * This calls provides an <code>DeflaterInputStream</code> implementation that produces
 * GZIP compressed content from another <code>InputStream</code>.
 *
 * @version 4.1
 */
public class GZIPCompressedInputStream extends DeflaterInputStream {

	/**
	 * This static class is used to hi-jack the InputStream read(b, off, len) function to be able
	 *   to compute the CRC32 checksum of the content as it is read.
	 */
	static private class CRCWrappedInputStream extends InputStream {
		private InputStream inputStream;

		/**
	     * CRC-32 of uncompressed data.
	     */
	    protected CRC32 crc = new CRC32();

	    /**
	     * Construct the object with the InputStream provided.
	     * @param pInputStream - Any class derived from InputStream class.
	     */
		public CRCWrappedInputStream(InputStream pInputStream) {
			inputStream = pInputStream;
			
			crc.reset(); // Reset the CRC value.
		}

		/**
		 * This group of methods are the InputStream equivalent methods that just 
		 *   call the method on the InputStream provided during construction.
		 */
		public int available() throws IOException { return inputStream.available(); };
		public void close() throws IOException { inputStream.close(); };
		public void mark(int readlimit) { inputStream.mark(readlimit); };
		public boolean markSupported() { return inputStream.markSupported(); };
		public int read() throws IOException { return inputStream.read(); };
		public int read(byte[] b) throws IOException { return inputStream.read(b); };
	    public void reset() throws IOException { inputStream.reset(); };
	    public long skip(long n) throws IOException { return inputStream.skip(n); };

	    /*
	     * This function intercepts all read requests in order to calculate the CRC
	     *   value that is stored in this object.
	     */
	    public int read(byte b[], int off, int len) throws IOException {
	    	// Do the actual read from the input stream
	    	int retval = inputStream.read(b, off, len);

	    	// If we successfully read something, compute the CRC value of it.
	    	if (0 <= retval) {
	    		crc.update(b, off, retval);
	    	}
	    	
	    	// All done with the intercept.  Return the value.
	    	return retval;
	    };
	    
	    /*
	     * Function to retrieve the CRC value computed thus far while the 
	     *   stream was processed. 
	     */
	    public long getCRCValue() { return crc.getValue(); };
	}

    /**
     * Creates a new input stream with the default buffer size of 512 bytes.
     * @param pInputStream - Input Stream to read content for compression
     * @throws IOException if an I/O error has occurred.
     */
    public GZIPCompressedInputStream(InputStream pInputStream) throws IOException {
    	this(pInputStream, 512);
    }
    
    /**
     * Creates a new input stream with the specified buffer size.
     * @param pInputStream - Input Stream to read content for compression
     * @param size the output buffer size
     * @exception IOException If an I/O error has occurred.
     */
    public GZIPCompressedInputStream(InputStream pInputStream, int size) throws IOException {
        super(new CRCWrappedInputStream(pInputStream), new Deflater(Deflater.DEFAULT_COMPRESSION, true), size);

        mCRCInputStream = (CRCWrappedInputStream) super.in;
    }


    // Indicator for if EOF has been reached for this stream.
    private boolean mReachedEOF = false;

    // Holder for the hi-jacked InputStream that computes the CRC-32 value
    private CRCWrappedInputStream mCRCInputStream;
	
    /*
     * GZIP Header structure and positional variable.
     */
    private final static int GZIP_MAGIC = 0x8b1f;

    private final static byte[] mHeader = {
         (byte) GZIP_MAGIC, // Magic number (short)
         (byte)(GZIP_MAGIC >> 8), // Magic number (short)
         Deflater.DEFLATED, // Compression method (CM)
         0, // Flags (FLG)
         0, // Modification time MTIME (int)
         0, // Modification time MTIME (int)
         0, // Modification time MTIME (int)
         0, // Modification time MTIME (int)
         0, // Extra flags (XFLG)
         0 // Operating system (OS)  FYI.  UNIX/Linux OS is 3
    };

    private int mHeaderPos = 0;  // Keeps track of how much of the header has already read.
    
    /*
     * GZIP Trailer structure and positional indicator.
     * 
     * Trailer consists of 2 integers: CRC-32 Value and Original File Size.
     */
    private final static int TRAILER_SIZE = 8;
    private byte mTrailer[] = null;
    private int mTrailerPos = 0;
    

    
    /***
     * Overridden functions against the DeflatorInputStream
     */

    
    /**
     * Function to indicate whether there is any content available to read. It is 
     *   overridden because there is the GZIP header and trailer to think about.
     */
    public int available() throws IOException {
		return (mReachedEOF ? 0 : 1);
	}

    /**
     * This read function is the meat of the class. It handles passing back GZIP Header, GZIP content, and GZIP Trailer
     *   in that order to the caller. 
     */
    public int read(byte[] outBuffer, int offset, int maxLength) throws IOException, IndexOutOfBoundsException {

    	int retval = 0; // Contains the number of bytes read into outBuffer and will be the return value of the function.
		int bIndex = offset;  // Used as current index into outBuffer.
		int dataBytesCount = 0; // Used to indicate how many data bytes are in the outBuffer array.
		
		// Make sure we have a buffer.
		if (null == outBuffer) {
            throw new NullPointerException("Null buffer for read");
        }
        
		// Make sure offset is valid.
		if (0 > offset || offset >= outBuffer.length)
		{
			throw new IndexOutOfBoundsException("Invalid offset parameter value passed into function");
		}
		
		// Make sure the maxLength is valid.
		if (0 > maxLength || outBuffer.length - offset < maxLength) 
			throw new IndexOutOfBoundsException("Invalid maxLength parameter value passed into function");
		
		// Asked for nothing, you get nothing.
		if (0 == maxLength)
			return retval;

		/**
		 * Put any GZIP Header in the buffer if we haven't already returned it from 
		 *   previous calls.
		 */
		if (mHeaderPos < mHeader.length)
		{
			// Get how much will fit.
			retval = Math.min(mHeader.length - mHeaderPos, maxLength);
			
			// Put it there.
			for (int i = retval; i > 0; i--)
			{
				outBuffer[bIndex++] = mHeader[mHeaderPos++];
			}
			
			// Return the number of bytes copied if we exhausted the maxLength specified. 
			//  NOTE: Should never be >, but...
			if (retval >= maxLength) {
				return retval;
			}
		}
		
		/**
		 * At this point, the header has all been read or put into the buffer.
		 * 
		 * Time to add some GZIP compressed data, if there is some still left.
		 */
		if (0 != super.available()) {
			
			// Get some data bytes from the DeflaterInputStream.
			dataBytesCount = super.read(outBuffer, offset+retval, maxLength-retval);
			
			// As long as we didn't get EOF (-1) update the buffer index and retval.
			if (0 <= dataBytesCount) {
				bIndex += dataBytesCount;
				retval += dataBytesCount;
			}
	
			// Return the number of bytes copied during this call, if we exhausted the maxLength requested. 
			//  NOTE: Should never be >, but...
			if (retval == maxLength) {
				return retval;
			}
			
			// If we got here, we should have read all that can be read from the Input Stream
			// So make sure the Input Stream is at EOF just in case someone tries to read it
			// outside this class.
			byte[] junk = new byte[1];
			if (-1 != super.read(junk, 0, junk.length)) {
				// Should never happen.  But you know how that goes.
				throw new IOException("Unexpected content read from input stream when EOF expected");
			}
		}

		/**
		 *  Got this far, time to write out the GZIP Trailer
		 */

		// Have we already setup the GZIP trailer in a previous invocation?
		if (null == mTrailer) {
			// Time to prepare the trailer.
			mTrailer = new byte[TRAILER_SIZE];
			
			// Put the content in it.
			writeTrailer(mTrailer, 0);
		}

		// If there are still GZIP Trailer bytes to be returned to the called, 
		//   do as much as will fit in the outBuffer.
		if (mTrailerPos < mTrailer.length) {
			
			// Get the number of bytes that will fit in the outBuffer.
			int trailerSize = Math.min(mTrailer.length - mTrailerPos, maxLength - bIndex);
			
			// Move them in.
			for (int i = trailerSize; i > 0; i--)
			{
				outBuffer[bIndex++] = mTrailer[mTrailerPos++];
			}
			
			// Return the total amount of bytes written during this call.
			return retval + trailerSize;
		}

		/**
		 * If we got this far, we have already been asked to read all content that is available.
		 * 
		 * So we are at EOF.
		 */
		mReachedEOF = true;
		return -1;
	}

	/***
	 * Helper functions to construct the trailer.
	 */
	
	/*
     * Writes GZIP member trailer to a byte array, starting at a given
     * offset.
     */
    private void writeTrailer(byte[] buf, int offset) throws IOException {
        writeInt((int)mCRCInputStream.getCRCValue(), buf, offset); // CRC-32 of uncompr. data
        writeInt(def.getTotalIn(), buf, offset + 4); // Number of uncompr. bytes
}

    /*
     * Writes integer in Intel byte order to a byte array, starting at a
     * given offset.
     */
    private void writeInt(int i, byte[] buf, int offset) throws IOException {
        writeShort(i & 0xffff, buf, offset);
        writeShort((i >> 16) & 0xffff, buf, offset + 2);
    }

    /*
     * Writes short integer in Intel byte order to a byte array, starting
     * at a given offset
     */
    private void writeShort(int s, byte[] buf, int offset) throws IOException {
        buf[offset] = (byte)(s & 0xff);
        buf[offset + 1] = (byte)((s >> 8) & 0xff);
    }
}

